Appearance
Java 泛型及其陷阱
泛型的诞生
在 Java 5 泛型出来之前,集合中保存的是通用类型 Object
。Java 单继承的结构意味着所有元素都基于 Object
类,所以在集合中可以保存任何类型的数据,易于重用。要使用这样的集合,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 Object
,当我们往集合中添加元素时,元素便向上转型成了 Object
,从而丢失自己原有的类型特性。这时我们再从集合中取出该元素时,元素的类型变成了 Object
。
java
// Java 5 之前
ArrayList arrayList = new ArrayList();
arrayList.add(12345);
arrayList.add("上山打老虎");
ListIterator listIterator = arrayList.listIterator();
while (listIterator.hasNext()) {
Object o = listIterator.next(); // 里面的元素都变成了 Object
}
那么,该怎么将其转回原先具体的类型呢?这里,我们使用了强制类型转换将其转为更具体的类型,这个过程称为对象的“向下转型”。可是我们不能从“Object”看出其就是“整数”或“字符串”,所以除非能确定元素的具体类型信息,否则“向下转型”就是不安全的。也不能说这样的错误就是完全危险的,因为一旦转化了错误的类型,程序就会运行出错,抛出“运行时异常”(RuntimeException
)。
java
Integer i = (Integer) arrayList.get(0);
Integer s = (Integer) arrayList.get(1); // java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合,以确认保存元素的具体类型,减少集合元素“向下转型”的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(Parameterized Type Mechanism)。
参数化类型机制可以使得编译器能够自动识别某个 class 的具体类型并正确地执行。举个例子,对集合的参数化类型机制可以让集合仅接受“字符串”这种类型的元素,并以“字符串”类型取出元素。Java 5 版本支持了参数化类型机制,称之为“泛型”(Generic)。
现在可以按以下方式向 ArrayList
中添加 String
(字符串):
java
// Java 5 之后
ArrayList<String> stringArrayList = new ArrayList<>();
stringArrayList.add("一二三四五");
stringArrayList.add("上山打老虎");
ListIterator<String> stringListIterator = stringArrayList.listIterator();
while (stringListIterator.hasNext()) {
String s = stringListIterator.next(); // 里面的元素还是原来的类型
}
泛型的陷阱
泛型应用在多态中可能会出现一些意想不到的问题。
首先创建父类和子类:
java
class Animal {
void eat() {
System.out.println("Animal eating...");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog eating...");
}
}
class Cat extends Animal {
@Override
void eat() {
System.out.println("Cat eating...");
}
}
多态化数组
我们先看一下数组参数是如何多态化运行的。
编写一个方法 takeAnimalWithArray
,接受参数为 Animal[]
,内容是遍历这个数组。
java
public static void takeAnimalWithArray(Animal[] animals) {
for (Animal animal : animals) {
animal.eat();
}
}
分别创建父类数组和子类数组作为参数调用 takeAnimalWithArray
。
父类数组:
javapublic static void main(String[] args) { Animal[] animals = {new Animal(), new Cat(), new Dog()}; takeAnimalWithArray(animals); }
编译并运行:
javaAnimal eating... Cat eating... Dog eating...
子类数组
javapublic static void main(String[] args) { Dog[] dogs = {new Dog(), new Dog(), new Dog()}; takeAnimalWithArray(dogs); }
编译并运行:
javaDog eating... Dog eating... Dog eating...
可以看到,方法参数为数组时,无论传入的引用是父类数组还是子类数组,编译和运行时一切正常。(真的正常吗?请往下看。)
那么把 Array
数组换成 ArrayList
集合后还会这样吗?
多态化集合
将数组替换为泛型集合。
编写一个方法 takeAnimalWithArrayList
,接受参数为 ArrayList<Animal>
,内容是遍历这个泛型集合。
java
public static void takeAnimalWithArrayList(ArrayList<Animal> animals) {
for (Animal animal : animals) {
animal.eat();
}
}
分别创建父类泛型集合和子类泛型集合作为参数调用 takeAnimalWithArrayList
。
父类泛型集合:
javapublic static void main(String[] args) { ArrayList<Animal> animals = new ArrayList<>(); animals.add(new Animal()); animals.add(new Cat()); animals.add(new Dog()); takeAnimalWithArrayList(animals); }
编译并运行:
javaAnimal eating... Cat eating... Dog eating...
子类泛型集合
javapublic static void main(String[] args) { ArrayList<Dog> dogs = new ArrayList<>(); dogs.add(new Dog()); dogs.add(new Dog()); dogs.add(new Dog()); takeAnimalWithArrayList(dogs); }
编译时:
java错误: 不兼容的类型: ArrayList<Dog>无法转换为ArrayList<Animal> takeAnimalWithArrayList(dogs); ^
想不到啊,看起来没有问题,编译时却报错。
本来用数组没有问题,改成集合为什么就不行了呢?要解决这个问题,我们先假设一下:如果编译可以通过会怎么样?
假如下面这个方法传入
ArrayList<Dog>
作为参数后程序可以编译通过:javapublic static void takeAnimalWithArrayList(ArrayList<Animal> animals) { for (Animal animal : animals) { animal.eat(); } }
那么程序应该是可以正常运行的(其实不能运行),就像多态化数组中的子类数组一样。
但如果方法是这样:
javapublic static void takeAnimalWithArrayList(ArrayList<Animal> animals) { animals.add(new Cat()); }
这就有问题了。理论上
ArrayList<Animal>
添加Cat
是合法的,毕竟Cat
是Animal
的子类。可是我们传入的其实是一个
ArrayList<Dog>
参数,将Cat
添加到一个ArrayList<Dog>
中肯定不合适,这就是为什么编译失败的原因。如果把方法参数定义为
ArrayList<Animal>
,它就只能传入ArrayList<Animal>
,ArrayList<Dog>
还是ArrayList<Cat>
都不行。
数组类型与集合类型的区别
同样的问题会发生在数组上吗?可以把 Cat
添加到一个 Dog[]
中吗?
java
public static void takeAnimalWithArray(Animal[] animals) {
animals[0] = new Cat();
}
可以编译,但运行时:
java
Exception in thread "main" java.lang.ArrayStoreException: Cat
at GenericsTest.takeAnimalWithArray(GenericsTest.java:42)
at GenericsTest.main(GenericsTest.java:28)
现在知道多态化数组中的子类数组为什么可以运行了。
数组的类型是在运行期间检查的,但集合的类型在编译期间就开始了检查。
怎么才能使用多态化集合参数呢?如何创建接受 Animal
子类的方法?
带有通配符与边界的泛型集合
使用上界通配符 <? extends T>
<? extends T>
声明 ?
必须是 T
或 T
的子类。 在这里我们需要接受 Animal
或 Animal
的子类,实际的参数为 ArrayList<? extends Animal>
。
java
public static void main(String[] args) {
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
takeAnimalWithArrayList(dogs);
ArrayList<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Cat());
animals.add(new Dog());
takeAnimalWithArrayList(animals);
}
public static void takeAnimalWithArrayList(ArrayList<? extends Animal> animals) {
for (Animal animal : animals) {
animal.eat();
}
}
编译和运行时一切正常。
相同功能的另一种语法
java
public static <T extends Animal> void takeAnimalWithArrayList(ArrayList<T> animals) {
for (Animal animal : animals) {
animal.eat();
}
}
有什么区别?这要看是否会用到 T
。
例如,有多个集合参数都继承 Animal
,只声明一次更有效率:
java
public static <T extends Animal> void takeAnimalWithArrayList(ArrayList<T> one, ArrayList<T> two) {
}
再比如,方法需要返回 T
:
java
public static <T extends Animal> T takeAnimalWithArrayList(ArrayList<T> animals) {
}
现在可以愉快的在方法中操作 ArrayList<Animal>
了,再添加一个 Dog
进来:
java
public static void takeAnimalWithArrayList(ArrayList<? extends Animal> animals) {
for (Animal animal : animals) {
animal.eat();
}
animals.add(new Dog());
}
编译时:
java
错误: 对于add(Dog), 找不到合适的方法
animals.add(new Dog());
^
为什么不能添加 Dog
?Cat
和 Animal
也不行吗?都不行,原因是 ArrayList<? extends Animal>
会将传递进来的类型自动向上转型为 Animal
,也就是说编译器只知道集合中的元素是 Animal
或 Animal
的子类,具体是什么类型不知道,往集合里面添加 Dog
,Cat
还是 Animal
编译器都不能保证能和原有的类型匹配,所以都不能添加。
在方法参数中使用上界通配符时,编译器会阻止任何试图改变引用参数所指集合的行为。也就是说,你可以获取集合元素,但不能新增集合元素(只能取,不能存,null
是个例外)。
可是需要添加 Dog
怎么办?
使用下界通配符 <? super T>
<? super T>
声明 ?
必须是 T
或 T
的父类。 在这里我们需要接受 Dog
或 Dog
的父类,实际的参数为 ArrayList<? super Dog>
。
java
public static void main(String[] args) {
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
takeAnimalWithArrayList(dogs);
ArrayList<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Cat());
animals.add(new Dog());
takeAnimalWithArrayList(animals);
}
public static void takeAnimalWithArrayList(ArrayList<? super Dog> dogs) {
dogs.add(new Dog());
}
编译和运行时一切正常。
现在可以愉快的在方法中操作 ArrayList<Dog>
了,再添加一个 Cat
进来:
java
public static void takeAnimalWithArrayList(ArrayList<? super Dog> dogs) {
dogs.add(new Dog());
dogs.add(new Cat());
}
编译时:
java
错误: 对于add(Cat), 找不到合适的方法
dogs.add(new Cat());
^
为什么不能添加 Cat
?Animal
也不行吗?都不行,虽然 ArrayList<? super Dog>
接受 Dog
或 Dog
的父类,但是会被编译器自动向下转型为 Dog
。由于 Cat
和 Animal
不是 Dog
的子类,所以,是不能往集合里面添加 Cat
的。
可是需要添加 Dog
又需要添加 Cat
甚至是 Animal
怎么办?将下界的范围扩大,使用 ArrayList<? super Animal>
:
java
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Cat());
animals.add(new Dog());
takeAnimalWithArrayList(animals);
}
public static void takeAnimalWithArrayList(ArrayList<? super Animal> animals) {
animals.add(new Dog());
animals.add(new Cat());
animals.add(new Animal());
}
编译和运行时一切正常。
现在可以愉快的在方法中操作 ArrayList<Animal>
了,遍历一下:
java
public static void takeAnimalWithArrayList(ArrayList<? super Animal> animals) {
animals.add(new Dog());
animals.add(new Cat());
animals.add(new Animal());
for (Animal animal : animals) {
animal.eat();
}
}
编译时:
java
错误: 不兼容的类型: CAP#1无法转换为Animal
for (Animal animal : animals) {
^
其中, CAP#1是新类型变量:
CAP#1从? super Animal的捕获扩展Object 超 Animal
1 个错误
为什么不能遍历呢?不能,原因是编译器已经将元素的类型都转成了 Object
,获取元素的时候不能保证能和原有的类型匹配,所以无法进行遍历。
在方法参数中使用下界通配符时,编译器会阻止任何试图获取引用参数所指集合元素的行为。也就是说,你可以新增集合元素,但不能获取集合元素(只能存,不能取,取出来的都是 Object
)。